from django.db import models
from django.utils import timezone
from hte.utils.general import get_current_username
from .material import MaterialCategory
from django.conf import settings
from django.core.validators import MinValueValidator
from django.contrib.postgres.fields import JSONField
from django.core.serializers.json import DjangoJSONEncoder
from enum import IntEnum, unique
from django.utils.translation import gettext as _
from django.utils.encoding import smart_text
from enum import IntEnum, Enum

from typing import Union, List, Tuple
from copy import deepcopy


class RangePart(Enum):
    LOWER_BOUND = 'lb'
    UPPER_BOUND = 'ub'
    VALUE = 'val'
    ERROR = 'err'


@unique
class TemplateFieldEnum(IntEnum):
    """模板字段枚举"""
    STRING = 1  # 字符串型
    NUMBER = 2  # 数值型
    RANGE = 3  # 范围型
    IMAGE = 4  # 图片型
    FILE = 5  # 文件型
    CHOICE = 6  # 候选型
    ARRAY = 7  # 数组型
    TABLE = 8  # 表格型
    CONTAINER = 9  # 容器型
    GENERATOR = 10  # 生成器型
    ERROR = 11  # 误差型

    @classmethod
    def min_value(cls):
        return cls._member_map_.get(cls._member_names_[0]).value

    @classmethod
    def max_value(cls):
        return cls._member_map_.get(cls._member_names_[-1]).value

    @classmethod
    def get_field_enum(cls, name_or_value):
        return cls._value2member_map_.get(name_or_value)

    @classmethod
    def get_name_by_value(cls, value):
        return cls._value2member_map_.get(value).name

    @classmethod
    def get_value_by_name(cls, name):
        return cls._value2member_map_.get(name).value

    @property
    def is_string(self):
        return self.value == TemplateFieldEnum.STRING

    @property
    def is_number(self):
        return self.value == TemplateFieldEnum.NUMBER

    @property
    def is_range(self):
        return self.value == TemplateFieldEnum.RANGE

    @property
    def is_image(self):
        return self.value == TemplateFieldEnum.IMAGE

    @property
    def is_file(self):
        return self.value == TemplateFieldEnum.FILE

    @property
    def is_choice(self):
        return self.value == TemplateFieldEnum.CHOICE

    @property
    def is_array(self):
        return self.value == TemplateFieldEnum.ARRAY

    @property
    def is_table(self):
        return self.value == TemplateFieldEnum.TABLE

    @property
    def is_container(self):
        return self.value == TemplateFieldEnum.CONTAINER

    @property
    def is_generator(self):
        return self.value == TemplateFieldEnum.GENERATOR

    @property
    def description(self):
        arr = (_("String"), _("Number"), _("Range"), _("Image"), _("File"), _("Choice"), _("Array"), _("Table"),
               _("Container"), _("Generator"),)
        return arr[self - 1]

    # yupeng加的开始
    @property
    def is_basic(self):
        return self in (
            TemplateFieldEnum.STRING,
            TemplateFieldEnum.NUMBER,
            TemplateFieldEnum.RANGE,
            TemplateFieldEnum.CHOICE
        )

    @staticmethod
    def is_range_interval(type_number):
        return type_number == 0

    @staticmethod
    def is_range_error(type_number):
        return type_number == 1

    # yupeng加的结束


class DocumentScope(IntEnum):
    """
    字段对应的搜索域（对应的MongoDB集合）
    """
    DATA_CONTENT = 0
    DATA_CONTAINER = 1
    DATA_TABLE_ROW = 2
    DATA_TABLE = 3


class TemplateField:
    """模板字段，屏蔽了模板底层的数据格式，提供了统一的访问模板字段的方法"""

    def __init__(self, field_type: (TemplateFieldEnum, int, str), required: bool = False, misc: dict = None):
        '''

        :param field_type:
        :param required:
        :param misc:
        :param field_dict: 字段原始dict
        :param field_path_list: 字段的完整路径list
        '''

        if isinstance(field_type, (int, str)):
            field_type = TemplateFieldEnum.get_field_enum(field_type)
        self.t = field_type
        self.r = required
        self.misc = misc

    def clean(self):
        if not isinstance(self.t, TemplateFieldEnum):
            raise ValueError('field type must be TemplateFieldEnum, not %s', self.t)
        if not isinstance(self.r, bool):
            raise ValueError('required must be boolean value, not %s', self.r)

        if self.t.is_string:
            self.misc = None
            return
        elif not isinstance(self.misc, dict):
            raise ValueError('misc must be an dict, not %s', self.misc)
        if self.t.is_number:
            if 'unit' in self.misc:
                self.misc = {'unit': smart_text(self.misc.get('unit'))}
            else:
                self.misc = None
        elif self.t.is_range:
            if self.misc.get('type') not in (0, 1):
                raise ValueError('range type must be 0 or 1, not %s', self.misc.get('type'))
            misc = {'type': self.misc.get('type')}
            if self.misc.get('unit'):
                misc['unit'] = smart_text(self.misc.get('unit'))
            self.misc = misc
        elif self.t.is_image or self.t.is_file:
            self.misc = {'multi': True if self.misc.get('multi') else False}
        elif self.t.is_choice:
            options = self.misc.get('opt')
            groups = self.misc.get('grp')
            if not (options or groups):  # 不同时为空
                raise ValueError('At lease one options/groups should be set.')
            true_options = []
            if options:
                if isinstance(options, (list, tuple)):
                    for opt in options:
                        true_options.append(smart_text(opt))
                else:
                    raise ValueError('Options should be list or tuple.')
            true_groups = []
            if groups:
                if isinstance(groups, (list, tuple)):
                    for grp in groups:
                        if isinstance(grp, dict) and ('name' in grp) and ('items' in grp) \
                                and isinstance(grp['items'], (list, tuple)):
                            true_grp = {'name': smart_text(grp['name']),
                                        'items': []}
                            if not grp['items']:
                                raise ValueError(
                                    "Group %s's items is empty, should add at least one item." % true_grp['name'])
                            for item in grp['items']:
                                true_grp['items'].append(smart_text(item))
                            true_groups.append(true_grp)
                        else:
                            raise ValueError('Groups should be list/tuple and has `name` and `items` key '
                                             'and `items` must be list/tuple.')
                else:
                    raise ValueError('Groups should be list/tuple.')
            self.misc = {'opt': true_options,
                         'grp': true_groups}
        elif self.t.is_array:
            item_field = TemplateField.from_dict(self.misc)
            self.misc = item_field
        elif self.t.is_table:
            if ('_head' not in self.misc) \
                    or (not isinstance(self.misc['_head'], (list, tuple))) or (not self.misc['_head']):
                raise ValueError('Head misc must be a `dict` and `_head` field must be list/tuple.')
            true_head_misc = {'_head': []}
            try:
                for head_name in self.misc['_head']:
                    true_head_name = smart_text(head_name)
                    true_head_misc['_head'].append(true_head_name)
                    true_head_field = TemplateField.from_dict(self.misc.get(true_head_name))
                    true_head_misc[true_head_name] = true_head_field
            except ValueError as ex:
                raise ValueError('Head(%s): %s' % (true_head_name, str(ex)))
            self.misc = true_head_misc
        elif self.t.is_container:
            if ('_ord' not in self.misc) \
                    or (not isinstance(self.misc['_ord'], (list, tuple))) or (not self.misc['_ord']):
                raise ValueError('Field misc must be a `dict` and `_ord` field must be list/tuple.')
            true_field_misc = {'_ord': []}
            try:
                for field_name in self.misc['_ord']:
                    true_field_name = smart_text(field_name)
                    true_field_misc['_ord'].append(true_field_name)
                    true_field = TemplateField.from_dict(self.misc.get(true_field_name))
                    true_field_misc[true_field_name] = true_field
            except ValueError as ex:
                raise ValueError('Field(%s): %s' % (true_field_name, str(ex)))
            self.misc = true_field_misc
        elif self.t.is_generator:
            if ('_opt' not in self.misc) \
                    or (not isinstance(self.misc['_opt'], (list, tuple))) or (not self.misc['_opt']):
                raise ValueError('Option misc must be a `dict` with `_opt` field in list/tuple form.')
            true_opt_misc = {'_opt': []}
            try:
                for field_name in self.misc['_opt']:
                    true_opt_name = smart_text(field_name)
                    true_opt_misc['_opt'].append(true_opt_name)
                    true_opt_field = TemplateField.from_dict(self.misc.get(true_opt_name))
                    true_opt_misc[true_opt_name] = true_opt_field
            except ValueError as ex:
                raise ValueError('Option(%s): %s' % (true_opt_name, str(ex)))
            self.misc = true_opt_misc
        else:
            raise ValueError('Field has illegal type "%s"' % self.t)

    def to_dict(self, clean=True):
        if clean:
            self.clean()
        d = {'t': self.t}  # 下面这样做只是为了节省存储空间
        if self.r:
            d['r'] = self.r
        if self.misc:
            d['misc'] = self.misc
        return d

    @staticmethod
    def from_dict(obj: dict):
        if isinstance(obj, TemplateField):
            return obj
        if not isinstance(obj, dict):
            raise ValueError('TemplateField obj must be an dict, not %s', obj)
        return TemplateField(obj.get('t'), obj.get('r', False), obj.get('misc'))


class TemplateJSONEncoder(DjangoJSONEncoder):
    """模板字段 encoder，主要是将 TemplateField 类转为对应的 dict """

    def default(self, o):
        if isinstance(o, TemplateField):
            return o.to_dict()
        return super(TemplateJSONEncoder, self).default(o)


class TemplateField_lyp:
    """模板字段，屏蔽了模板底层的数据格式，提供了统一的访问模板字段的方法"""

    def __init__(self, field_dict: dict, field_path_list: Union[List[str], Tuple[str]], parent_field=None,
                 is_array_element=False, array_depth=0, field_scope: DocumentScope = DocumentScope.DATA_CONTENT,
                 direct_array_parent=None, range_part=None, exclude_list=(), include_list=(), force_include=False):
        """
        :param field_dict: 字段原始dict
        :param field_path_list: 字段的完整路径list
        :param is_array_element: 字段本身是否是数组的元素
        :param array_depth: 数组深度
        """
        self.delay_excluded = False
        self._field_path_str_field_map = {}
        self._sub_field_name_field_map = {}
        self._t = TemplateFieldEnum(field_dict['t'])
        self._scope = field_scope
        self._self_in_array = is_array_element
        self._misc = field_dict.get('misc', {})
        self._r = field_dict.get('r', False)
        self._parent_field = parent_field
        self._field_path_list = field_path_list
        self._field_path_str = '.'.join([str(x) for x in field_path_list])
        self._parent_path_str = '.'.join([str(x) for x in field_path_list[:-1]])

        self._direct_array_parent = direct_array_parent
        self._range_part = range_part
        self._is_embedded_in_array = array_depth > 0
        if not force_include:
            if exclude_list and self._field_path_str in exclude_list or include_list and self._field_path_str not in include_list:
                self.delay_excluded = True
                return

        if self._t.is_container or self._t.is_generator:
            sub_scope = DocumentScope.DATA_CONTAINER
        elif self._t.is_table:
            sub_scope = DocumentScope.DATA_TABLE_ROW
        else:
            sub_scope = field_scope

        if is_array_element:
            self._field_name = field_path_list[-2]
        else:
            self._field_name = field_path_list[-1]
        self._field_path_str_for_display = '.'.join(filter(lambda x: isinstance(x, str), field_path_list))

        self._sub_fields = []

        force_include = force_include or include_list and self._field_path_str in include_list

        if self._t.is_array:

            f = TemplateField_lyp(self._misc, self._field_path_list + [self._misc['t']],
                                  parent_field=self,
                                  is_array_element=True,
                                  field_scope=sub_scope,
                                  array_depth=array_depth + 1,
                                  direct_array_parent=self,
                                  force_include=force_include)

            if f.delay_excluded:
                self.delay_excluded = True
                return

            self._sub_fields.append(f)
            self._field_path_str_field_map[f._field_path_str] = f
            self._field_path_str_field_map.update(f._field_path_str_field_map)
            del f._field_path_str_field_map

        elif self._t.is_range and range_part is None:

            if self.range_type == 0:
                child1 = TemplateField_lyp(field_dict, field_path_list + ['lb'], parent_field, is_array_element,
                                           array_depth, field_scope, direct_array_parent, RangePart.LOWER_BOUND)
                child2 = TemplateField_lyp(field_dict, field_path_list + ['ub'], parent_field, is_array_element,
                                           array_depth,
                                           field_scope, direct_array_parent, RangePart.UPPER_BOUND)
            else:
                child1 = TemplateField_lyp(field_dict, field_path_list + ['val'], parent_field, is_array_element,
                                           array_depth, field_scope, direct_array_parent, RangePart.VALUE)
                child2 = TemplateField_lyp(field_dict, field_path_list + ['err'], parent_field, is_array_element,
                                           array_depth,
                                           field_scope, direct_array_parent, RangePart.ERROR)

            self._field_path_str_field_map[child1._field_path_str] = child1
            self._field_path_str_field_map[child2._field_path_str] = child2

        else:
            is_embedded = False
            if exclude_list or include_list:
                misc_copy = deepcopy(self._misc)
            else:
                misc_copy = {}

            def remove_sub_field(sub_field_name):
                misc_copy.pop(sub_field_name, None)
                if self._t.is_generator:
                    sub = misc_copy['_opt']
                elif self._t.is_table:
                    sub = misc_copy['_head']
                elif self._t.is_container:
                    sub = misc_copy['_ord']
                else:
                    sub = []
                sub.remove(sub_field_name)

            for sub_field_name in self.sub_field_names:
                f = TemplateField_lyp(self._misc[sub_field_name], self._field_path_list + [sub_field_name],
                                      parent_field=self,
                                      is_array_element=False,
                                      field_scope=sub_scope,
                                      array_depth=array_depth,
                                      direct_array_parent=direct_array_parent,
                                      force_include=force_include)
                if f.delay_excluded:
                    remove_sub_field(sub_field_name)
                    continue
                self._sub_fields.append(f)
                self._sub_field_name_field_map[f.field_name] = f
                self._field_path_str_field_map[f._field_path_str] = f
                self._field_path_str_field_map.update(f._field_path_str_field_map)
                del f._field_path_str_field_map
                is_embedded = True
            if is_embedded and not len(self._sub_fields):
                self.delay_excluded = True
            if exclude_list or include_list:
                self._misc = misc_copy

    def __getitem__(self, item: Union[str, int]):
        """
        提供下标访问方式，如果下标i是数字则返回第i个子字段，是字符串则返回名称为i的子字段
        :param item: 子字段的名称或者序号（0开始）
        :return: 子字段TemplateField
        """
        if isinstance(item, str):
            return self._sub_field_name_field_map[item]
        else:
            return self._sub_fields[item]

    def __unicode__(self):
        return f"{self.field_path_str}: {self.field_type.description}"

    def __str__(self):
        return self.__unicode__()

    def __repr__(self):
        return self.__unicode__()

    def __eq__(self, other):
        if not isinstance(other, TemplateField_lyp):
            return False
        return self._field_path_str == other._field_path_str

    def __ne__(self, other):
        return not self == other

    @property
    def document_scope(self):
        return self._scope

    @property
    def document_path(self):
        if self.is_array_element:
            return self.parent_field.document_path
        elif self.parent_field is not None:
            return self.parent_field.field_path_str
        else:
            return '/'  # DataContent的字段，认为其_path是"/"

    @property
    def is_embedded_in_array(self):
        return self._is_embedded_in_array

    @property
    def direct_array_or_table_parent(self) -> 'TemplateField_lyp':
        if self.is_table_row:
            return self._parent_field
        return self._direct_array_parent

    @property
    def is_table_row(self):
        if self._parent_field:
            return self._parent_field.field_type.is_table

    @property
    def sub_fields(self) -> List["TemplateField_lyp"]:
        return self._sub_fields

    @property
    def parent_field(self) -> "TemplateField_lyp":
        return self._parent_field

    @property
    def is_array_element(self) -> bool:
        return self._self_in_array

    @property
    def parent_is_array_element(self) -> bool:
        return False if self._parent_field is None else self._parent_field.is_array_element

    @property
    def sub_field_names(self) -> List[str]:
        if self._t is None or self._t.is_container:
            sub_name_list = '_ord'
        elif self._t.is_generator:
            sub_name_list = '_opt'
        elif self._t.is_table:
            sub_name_list = '_head'
        else:
            return []
        return self._misc.get(sub_name_list, [])

    @property
    def external_field_path_str(self):
        return self._field_path_str_for_display

    @property
    def field_path_str(self):
        return self._field_path_str

    @property
    def field_type(self):
        return self._t

    @property
    def field_name(self):
        return self._field_name

    @property
    def misc(self):
        return self._misc

    @property
    def unit(self):
        return self._misc.get('unit')

    @property
    def element_field(self):
        if self._t.is_array:
            return self._sub_fields[0]

    @property
    def choices_list(self):
        if self._t.is_choice:
            choices = []
            choices.extend(self._misc.get('opt', []))
            for group in self._misc.get('grp', []):
                choices.extend(group.get('items', []))
            return choices

    @property
    def required(self):
        return self._r

    @property
    def range_type(self):
        return self._misc.get('type')

    @property
    def has_multiple_files(self):
        return self._misc.get('multi') or False

    @property
    def dict_for_export(self):
        j = {'_type': self.field_type.description}
        if self.unit:
            j['_unit'] = self.unit
        for sub_field in self.sub_fields:
            j[sub_field.field_name] = sub_field.dict_for_export
        if self.field_type.is_array:
            j[self.field_name] = self.element_field.dict_for_export
        return j

    def to_dict(self):
        return {'t': self._t.value, 'misc': self._misc}

    @property
    def field_path_str_field_map(self):
        return self._field_path_str_field_map

    @property
    def range_part(self):
        return self._range_part

    def remove_sub_field(self, field_path_str):
        pass


class Template(models.Model):
    """模板类，包含模板元数据和模板内容`content`部分。

    `title`:     模板标题，标题不能重复
    `category`:  所属材料类别，引用`MaterialCategory`
    `author`:    创建者名字，对应 account 中的用户名（无需显示设置，默认为当前登录用户，未登录则为 '_system'）
    `abstract`:  简要介绍，包括模板内容、功能、作用等
    `ref_count`: 引用计数，即使用这个模板的数据的数目
    `pub_date`:  发布时间
    `published`: 是否发布
    `trashed`:   是否已删除
    `content`:   模板内容，一个复杂的字典类型数据，详见文档示例
    """
    id = models.AutoField(primary_key=True)
    title = models.CharField(unique=True, max_length=255)
    category = models.ForeignKey(MaterialCategory,
                                 null=True,
                                 on_delete=models.SET_NULL,
                                 limit_choices_to={'leaf': True})
    author = models.CharField(max_length=20, default=get_current_username)##更改default为当前用户名（fsch)
    abstract = models.CharField(max_length=512)
    ref_count = models.IntegerField(default=0, validators=(MinValueValidator(0),))
    pub_date = models.DateTimeField(default=timezone.now)
    published = models.BooleanField(default=False)
    trashed = models.BooleanField(default=False)
    content = JSONField(encoder=TemplateJSONEncoder)

    class Meta:
        ordering = ('-pub_date',)

    def __str__(self):
        return self.title

    def delete(self, *args, **kwargs):
        """如果引用计数大于 0 则仅打上删除标签"""
        if self.ref_count:
            Template.objects.filter(pk=self.pk).update(trashed=True)
        else:
            super(Template, self).delete(*args, **kwargs)

    def set_content(self, content: dict):
        """
        设置模板的 content 内容，会进行验证
        :param content: content内容
        :return: None
        """
        if ('_ord' not in content) or (not content['_ord']) or (not isinstance(content['_ord'], (list, tuple))):
            raise ValueError('Content must has `_ord` in list/tuple type and cannot be empty.')
        true_content = {'_ord': content['_ord']}
        try:
            for field_name in content['_ord']:
                field = content.get(field_name)
                if isinstance(field, dict):
                    true_content[field_name] = TemplateField.from_dict(field)
                elif not isinstance(field, TemplateField):
                    raise ValueError('Field value(%s) is invalid.' % str(field))
                else:
                    true_content[field_name] = field
        except ValueError as ex:
            raise ValueError('Field "%s": %s' % (field_name, str(ex)))
        else:
            self.content = true_content

    def clean(self):
        self.set_content(self.content)
        super(Template, self).clean()

    def to_dict(self, meta_only=False):
        self.get_deferred_fields()
        d = {'id': self.id,
             'title': self.title,
             'category': self.category.name,
             'category_id': self.category_id,
             'author': self.author,
             'abstract': self.abstract,
             'ref_count': self.ref_count,
             'pub_date': self.pub_date,
             'published': self.published}  # trashed 永远不被外部获取不予设置
        if not meta_only:
            d['content'] = self.content
        return d

    @staticmethod
    def inc_ref(t_or_pk, n=1):
        """
        将`template`的引用计数 + n（更新到数据库）
        :param t_or_pk: 待更新的模板或模板 pk
        :type t_or_pk: Template, int
        :param n: 需要增加的计数值，默认为 1
        :type n: int
        :return: None
        """
        if isinstance(t_or_pk, Template):
            t_or_pk.ref_count += n
            t_or_pk.save(update_fields=('ref_count',))
        else:
            Template.objects.filter(pk=t_or_pk).update(ref_count=models.F('ref_count') + n)

    @staticmethod
    def dec_ref(t_or_pk, n=1):
        """
        将`template`的引用计数 - n（更新到数据库）
        :param t_or_pk: 待更新的模板或模板 pk
        :type t_or_pk: Template, int
        :param n: 需要减少的计数值，默认为 1
        :type n: int
        :return: None
        """
        _t = t_or_pk
        if not isinstance(_t, Template):
            _t = Template.objects.filter(pk=t_or_pk).only('ref_count').first()
        if _t:
            _t.ref_count = max(0, _t.ref_count - n)
            _t.save(update_fields=('ref_count',))

    @staticmethod
    def publish(t_or_pk):
        """
        发布模板
        :param t_or_pk: 待更新的模板或模板 pk
        :type t_or_pk: Template, int
        :return: None
        """
        _t = t_or_pk
        if not isinstance(_t, Template):
            _t = Template.objects.filter(pk=t_or_pk).only('published').first()
        if _t:
            _t.published = True
            _t.save(update_fields=('published', 'pub_date'))

    def get_field_by_path(self, field_path_list: list, get_array_element=True):
        def get_meta(field_path, content):
            parent = content[field_path[0]]
            parent_t = TemplateFieldEnum(parent['_t'])
            if parent_t.is_array:
                if not get_array_element:
                    return parent
            if parent_t.is_array or parent_t.is_container or parent_t.is_table or parent_t.is_generator:
                return get_meta(field_path[1:], parent["_misc"])
            return parent

        return get_meta(field_path_list, self.content)

    '''
    开始：Yupeng 为了实现搜索功能而添加的内容
    '''

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._fields = None
        self._field_path_str_field_map = None

    def _load_fields(self, exclude_list=(), include_list=()):
        self._fields = []
        self._field_path_str_field_map = {}
        if exclude_list or include_list:
            content_copy = deepcopy(self.content)

        def remove_sub_field(sub_field_name):
            content_copy['_ord'].remove(sub_field_name)
            content_copy.pop(sub_field_name, None)

        for field_name in self.content['_ord']:
            if exclude_list and field_name in exclude_list or include_list and field_name not in include_list:
                remove_sub_field(field_name)
                continue
            f = TemplateField_lyp(self.content[field_name], field_path_list=[field_name],
                                  exclude_list=exclude_list, include_list=include_list, force_include=True)
            if f.delay_excluded:
                remove_sub_field(field_name)
                continue
            self._fields.append(f)
            self._field_path_str_field_map[f.field_path_str] = f
            self._field_path_str_field_map.update(f.field_path_str_field_map)
            del f._field_path_str_field_map

        if exclude_list or include_list:
            self.content = content_copy

    @property
    def fields(self) -> List[TemplateField_lyp]:
        """
        获取子字段元数据列表
        :return: 子字段元数据列表
        """
        if self._fields is None:
            self._load_fields()
        return self._fields

    @property
    def dict_for_export(self):
        j = {'_id': self.id}
        for field in self.fields:
            j[field.field_name] = field.dict_for_export
        return j

    '''
    结束：Yupeng 为了实现搜索功能而添加的内容
    '''
